package com.gy.boot.common.dao;

import cn.hutool.core.util.ReUtil;
import com.gy.boot.common.util.BlankUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;

/**
 * MyBatis查询SQL拦截器, 拦截所有SQL
 * query: 所有查询SQL: SELECT
 * update: 所有 增: INSERT, 删: DELETE, 改: UPDATE
 * */
@Intercepts({
  @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
, @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(SqlInterceptor.class);

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("========= MyBatis SqlInterceptor 4 =========");
        final Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1]; // SQL参数
        BoundSql boundSql = ms.getBoundSql(parameter);
        Configuration configuration = ms.getConfiguration();
        String sqlStr = MySqlParser.formatSqlStr(boundSql.getSql()); // 拦截器-格式化SQL字符串
        String sqlID = ms.getId();  // SQL_ID
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        if (!SqlCommandType.SELECT.equals(sqlCommandType)) {
            printSqlLog(sqlID, sqlStr, boundSql, configuration); // 修改数据, 打印SQL语句
            return invocation.proceed();
        }
        // 获取用户是否添加权限;
        String accessSql = SqlAccess.getAccessSql(sqlID, sqlStr);
        sqlStr = SqlAccess.addAccessSql(sqlStr, accessSql); // SQL添加权限
        // 判断是否需要分页
        IPage<?> page = IPageHelp.getLocalPage();
        // 没有分页, 直接返回
        if (page == null) {
            if (BlankUtil.isBlank(accessSql)) {
                printSqlLog(sqlID, sqlStr, boundSql, configuration); // 直接查询, 打印SQL
            } else {
                updateSql(invocation, sqlStr);
            }
            return invocation.proceed();
        }
        // 处理分页数据
        execSqlPage(invocation, sqlStr, accessSql, page);
        return page;
    }

    /**
     * 处理分页数
     * @param invocation 执行器
     * */
    private void execSqlPage(Invocation invocation, String sqlStr, String accessSql, IPage page) throws Exception {
        if (page.isCount()) {
            final Object[] args = invocation.getArgs();
            final MappedStatement ms = (MappedStatement) args[0];
            // 处理分页 COUNT
            selectPageCount(invocation, sqlStr, accessSql, page);
            boolean isNotSelectLimit = page.getTotal() <= page.getStart();
            if (isNotSelectLimit) {
                return;
            }
            // 恢复类型返回类型
            args[0] = ms;
        }
        String limitSqlStr = sqlStr + " LIMIT " + (page.getStart() == 0 ? page.getEnd() : (page.getStart() + ", " + page.getStart()));
        updateSql(invocation, limitSqlStr);
        Object resultObj = invocation.proceed();
        page.addAll((ArrayList) resultObj);
    }

    /**
     * 获取分页count查询语句
     * */
    private static void selectPageCount(Invocation invocation, String sqlStr, String accessSql, IPage page) throws Exception {
        String countSql;
        String countSqlID = null;
        if (BlankUtil.isBlank(page.getCountSqlID())) {
            countSql = MySqlParser.getCountSqlStr(sqlStr);
        } else { // 从 SQL_XML 中 获取 执行SQL;
            final Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Configuration configuration = ms.getConfiguration();
            MappedStatement countMs = configuration.getMappedStatement(page.getCountSqlID());
            Object parameter = args[1]; // SQL参数
            countSql = countMs.getBoundSql(parameter).getSql();
            countSql = MySqlParser.formatSqlStr(countSql); // 格式化countSql
            countSql = SqlAccess.addAccessSql(countSql, accessSql); // count查询添加SQL权限
            if (BlankUtil.isBlank(countSql)) {
                return;
            }
            countSqlID = page.getCountSqlID();
        }
        updateSqlCount(invocation, countSql, countSqlID);
        Object countResultList = invocation.proceed();
        int total = (int) ((List) countResultList).get(0);
        page.setTotal(total);
    }

/**  ===================================== 修改SQL语句 =========================================== */
    private static void updateSql(Invocation invocation, String sql) {
        updateSql(invocation, sql, false, null);
    }
    private static void updateSqlCount(Invocation invocation, String sql, String countSqlID) {
        updateSql(invocation, sql, true, countSqlID);
    }
    private static void updateSql(Invocation invocation, String sql, boolean isCount, String countSqlID) {
        final Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object params = args[1];
        BoundSql boundSql = ms.getBoundSql(params);
        countSqlID = isCount ? (BlankUtil.isBlank(countSqlID) ? ms.getId() + _PageCount : countSqlID) : ms.getId();
        MappedStatement newStatement = newMappedStatement(ms, boundSql, isCount, countSqlID);
        MetaObject msObject = SystemMetaObject.forObject(newStatement);
        msObject.setValue(sqlSource_boundSql_sql, sql);
        args[0] = newStatement;
        printSqlLog(countSqlID, sql, boundSql, ms.getConfiguration()); // 修改SQL-后打印SQL
    }
    private static final String _PageCount = "_PageCount";
    private static final String sqlSource_boundSql_sql = "sqlSource.boundSql.sql";
    private static MappedStatement newMappedStatement(MappedStatement ms, BoundSql boundSql, boolean isCount, String countSqlID) {
        SqlSource newSqlSource = new BoundSqlSqlSource(boundSql);
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), countSqlID, newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        if (isCount) { // count查询返回值int
            List<ResultMap> resultMaps = new ArrayList<>();
            ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Integer.class, EMPTY_RESULT_MAPPING).build();
            resultMaps.add(resultMap);
            builder.resultMaps(resultMaps);
        }
        return builder.build();
    }
    private static final List<ResultMapping> EMPTY_RESULT_MAPPING = new ArrayList<>(0);
    private static class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;
        BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }
        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }


/*** ======================================== 打印SQL语句 ================================================================ **/
    private static final boolean PrintSqlLog = false; // 是否打印SQL 语句
    private static void printSqlLog(String sqlID, final String sqlStr, BoundSql boundSql, Configuration configuration) {
        String sqlStr2 = getSqlBindingParameters(sqlStr, boundSql, configuration); // 绑定参数
        sqlStr2 = sqlID + " ==> " + sqlStr2;
        if (PrintSqlLog) {
            logger.info(sqlStr2);
        } else {
            System.out.println(sqlStr2);
        }
    }
    // SQL语句中的占位符?, 若WHERE条件中有'?'会导致冲突, 可根据实际应用情况,对截取SQL逻辑进行调整
    private static final String SqlParamsPlaceholder = "?";
    private static final String SqlParamsPlaceholder_T = "|A-Q_M|"; // A question mark 非参数?进行替换
    private static String getSqlBindingParameters(String sqlStr, BoundSql boundSql, Configuration configuration) {
        if (BlankUtil.isBlank(sqlStr) || !sqlStr.contains(SqlParamsPlaceholder) || boundSql == null) {
            return sqlStr;
        }
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();
        if (parameterMappings == null || parameterMappings.isEmpty()) {
            return sqlStr;
        }
        // 非参数"?"进行替换
        sqlStr = sqlParamsPlaceholderTo(sqlStr, false);
        // 绑定参数
        for (ParameterMapping parameterMapping : parameterMappings) {
            int index = sqlStr.indexOf(SqlParamsPlaceholder);
            if (index < 0) {
                break;
            }
            if (parameterMapping.getMode() == ParameterMode.OUT) {
                continue;
            }
            Object value; // 获取参数
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                value = configuration.newMetaObject(parameterObject).getValue(propertyName);
            }
            // 字符串类型的数据, 需要添加上引号;
            if (value instanceof String) {
                value = "'" + value + "'";
            }
            String valStr = sqlParamsPlaceholderTo(String.valueOf(value), true);
            sqlStr = sqlStr.substring(0, index) + valStr + sqlStr.substring(index + 1);
        }
        // 将非参数"?"替换回来
        return sqlParamsPlaceholderT(sqlStr);
    }
    /** 将非参数"?"替换回来 */
    private static String sqlParamsPlaceholderT(String sqlStr) {
        if (sqlStr.contains(SqlParamsPlaceholder_T)) {
            return sqlStr.replace(SqlParamsPlaceholder_T, SqlParamsPlaceholder);
        }
        return sqlStr;
    }
    private final static Pattern SingleQuotes = Pattern.compile("\'([^\']*)\'"); // 单引号正则
    /** 替换非参数"?" */
    private static String sqlParamsPlaceholderTo(String sqlStr, boolean isParam) {
        if (sqlStr.contains(SqlParamsPlaceholder)) {
            if (isParam) {
                return sqlStr.replace(SqlParamsPlaceholder, SqlParamsPlaceholder_T);
            }
            // 查询 ? 是否存在 '' 中
            List<String> listSingleQuotesStr = ReUtil.findAllGroup0(SingleQuotes, sqlStr);
            for (String str : listSingleQuotesStr) {
                if (str.contains(SqlParamsPlaceholder)) {
                    String str2 = str.replace(SqlParamsPlaceholder, SqlParamsPlaceholder_T);
                    sqlStr = sqlStr.replace(str, str2);
                }
            }
        }
        return sqlStr;
    }
}
